遇到多國語系最麻煩的無非就是 key 該如何訂定?有些人覺得應該語意化,有些人覺得應該直接按照檔案路徑,在激烈的爭辯後,考量到未來的擴充性,我們最後決定採用檔案結構為依歸,並且預設語言一律為英文。我們是使用 react-intl
這個套件實作多國語系,當初在 i18n 跟 intl 上決定了許久,由於 i18n 只將重點擺在語言上,並不包含日期時間、幣值、數字表示法等不同國家文化的國際化,因此最後選擇了比較廣用的 react-intl
。
我們的資料夾結構如下:
src/
components/
common/
component1.tsx
component2.tsx
translation.ts
featureA/
componentA1.tsx
componentA2.tsx
componentA3.tsx
translation.ts
featureB/
componentB1.tsx
componentB2.tsx
translation.ts
每個 feature(domain) 資料夾都會有一個翻譯檔案,因為對於不同的 feature(domain) 來說,context 也會差異很大。如果是有些共用的元件,像是按鈕、Modal 等等,則是直接使用 common 這個共用的 feature(domain),因此 translation 檔案的管理也比較容易。
在 react 裡面習慣會將頁面切分出來管理,上面描述的是關於元件層級的翻譯管理,至於頁面層級的翻譯管理,我們使用以下結構:
src/
pages/
translation.ts
PageA/
index.ts
PageA.tsx
PageComponentA1.tsx
PageComponentA2.tsx
PageB/
index.ts
PageB.tsx
PageComponentB1.tsx
由於 react 大部分都是元件化的關係,因此我們不需要每個 page 裡面都有 translation 檔案,這樣管理上複雜度會增加不少,因此我們統一使用一個 translation 檔案來管理頁面層級的翻譯。
由於我們是依照檔案位置來命名,加上我們為了確保一致性,因此統一採用三層命名:{domain}.{component}.{item}
,如下所示:
// src/components/activity/translation.ts
import { defineMessages } from "react-intl";
const activityMessages = {
"*": defineMessages({
add: { id: "activity.*.add", defaultMessage: "新增" },
}),
ActivitySessionItem: defineMessages({
attended: {
id: "activity.ActivitySessionItem.attended",
defaultMessage: "已簽到",
},
attendNow: {
id: "activity.ActivitySessionItem.attendNow",
defaultMessage: "立即簽到",
},
enterLinkPage: {
id: "activity.ActivitySessionItem.enterLinkPage",
defaultMessage: "進入直播頁面",
},
add: { id: "activity.ActivitySessionItem.add", defaultMessage: "新增" },
}),
};
export default activityMessages;
比較特別的是如果同一個 featur(domain) 裡面的元件有共用的部分,像是「儲存」、「取消」,這種共用部分我們會使用 *
的方式來命名,如此一來也可以避免一直在寫相同的 key。如果是頁面層級的翻譯的話則類似,只是 feature(domain) 的部分統一使用:page。
// src/pages/translation.ts
import { defineMessages } from "react-intl";
const pageMessages = {
"*": defineMessages({
cancel: { id: "page.*.cancel", defaultMessage: "取消" },
}),
// ProgramContentPage
ProgramContentPage: defineMessages({
foo: { id: "page.ProgramContentPage.foo", defaultMessage: "Foo Message" },
bar: { id: "page.ProgramContentPage.bar", defaultMessage: "Bar Message" },
}),
ProgramContentTabs: defineMessages({
foo: { id: "page.ProgramContentTabs.foo", defaultMessage: "Foo Message" },
bar: { id: "page.ProgramContentTabs.bar", defaultMessage: "Bar Message" },
}),
// ProgramPage
ProgramPage: defineMessages({
foo: { id: "page.ProgramPage.foo", defaultMessage: "Foo Message" },
bar: { id: "page.ProgramPage.bar", defaultMessage: "Bar Message" },
}),
};
export default pageMessages;